-
Notifications
You must be signed in to change notification settings - Fork 63
RFD-322 -- /v1/ Disks API
#2008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
nexus/src/app/instance.rs
Outdated
| let (.., authz_project, authz_disk, _) = | ||
| LookupPath::new(opctx, &self.db_datastore) | ||
| .organization_name(organization_name) | ||
| .project_name(project_name) | ||
| .disk_name(disk_name) | ||
| .fetch() | ||
| .await?; | ||
| let (.., authz_instance, _) = | ||
| LookupPath::new(opctx, &self.db_datastore) | ||
| .project_id(authz_project.id()) | ||
| .instance_name(instance_name) | ||
| .fetch() | ||
| .await?; | ||
|
|
||
| let (.., authz_instance) = | ||
| instance_lookup.lookup_for(authz::Action::Modify).await?; | ||
| let (.., authz_disk) = | ||
| disk_lookup.lookup_for(authz::Action::Modify).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The permissions here before were wrong I believe. It was doing a fetch call, which was unnecessary, and not using the right permissions.
nexus/src/app/instance.rs
Outdated
| let (.., authz_project, authz_disk, _) = | ||
| LookupPath::new(opctx, &self.db_datastore) | ||
| .organization_name(organization_name) | ||
| .project_name(project_name) | ||
| .disk_name(disk_name) | ||
| .fetch() | ||
| .await?; | ||
| let (.., authz_instance, _) = | ||
| LookupPath::new(opctx, &self.db_datastore) | ||
| .project_id(authz_project.id()) | ||
| .instance_name(instance_name) | ||
| .fetch() | ||
| .await?; | ||
|
|
||
| let (.., authz_instance) = | ||
| instance_lookup.lookup_for(authz::Action::Modify).await?; | ||
| let (.., authz_disk) = | ||
| disk_lookup.lookup_for(authz::Action::Modify).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the attach case. Doing an unnecessary fetch and not using the right permissions.
| #[endpoint { | ||
| method = GET, | ||
| path = "/v1/disks", | ||
| tags = ["disks"], | ||
| }] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning
I've taken liberties here. This code warrants extra attention.
I've collapsed down the different ways of listing disks into a single endpoint
Previously we had
GET /.../projects/{project}/disksGET /.../instances/{instance}/disks
Essentially this allows listing all disks in the first case and only disks attached to a particular instance in the second case. I've collapsed this down into a single endpoint.
GET /v1/disks?project={project_id}GET /v1/disks?instance={instance_id}
If you provide the instance selector then it filters down to disks attached to the instance. Otherwise it gives you all disks in the projects. That makes this endpoint have slightly different requirement semantics on its selectors. In general the selector for the parent resource (project in this case) is required for list endpoints. Because the selector here can be either an instance or project (and their subsequent paths) all the selectors are optional.
Overall I think the usage of this is simplified and it gives one place to go to list disks. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In line with #2008 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see the appeal. I'm not sure I grok the implications but it sounds reasonable!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, well, I did an about face. This no longer applies. The endpoint is now /v1/instances/{instance}/disks which maps to what we had previously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm fine with either. You had me convinced (and enthusiastic!) about a single endpoint with multiple search criteria, but I'm fine with the "about face". What changed your mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few reasons:
- Keeping
/diskson the end of instances is consistent with the current API so this is the minimal viable change. This is the main one. - A consequence of being able to select on
instanceorprojectis that they both had to be optional. That just means there's an extra failure mode here. There's also a bit more to document.
I will say that I like the single endpoint better (what I had) than I like having two endpoints returning the same type of things at different levels of the API (what's there now and before this PR). If we already have a place that we're interacting with disks associated with an instance though, it probably just makes sense to add the listing there.
Overall I would be good to move back to the single disk list endpoint, but we'd need to do something with the attach/detach APIs. I don't think we'd want to have instances/{}/disks/(attach|detach) because the /disks part would be hanging with no valid methods on it. If we dropped /disks like @karencfv original suggestion then /instances/{}/(attach|detach) may be too vague. What are you attaching or detaching? Lastly I just wasn't a fan of chaining names together where we don't have to /disks-attach. I really couldn't think of a better alternative so I just went back to the original.
| /// Attach a disk to an instance | ||
| #[endpoint { | ||
| method = POST, | ||
| path = "/v1/disks/{disk}/attach", | ||
| tags = ["disks"], | ||
| }] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning
This section warrants extra attention.
With attach and detach actions I took the same sort of approach as I did with listing in that I collapsed it down to the disks endpoint.
Before:
POST /.../instances/{instance}/disks/attach { disk: Name }POST /.../instances/{instance}/disks/detach { disk: Name}
After
/v1/disks/{disk}/attach { instance: NameOrId }/v1/disks/{disk}/detach { instance: NameOrId }
Given the overall flatter structure of the API I think this mapping makes slightly more sense. Input welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately I don't think this approach will be as clear as the previous one. You generally attach a disk to an instance(vm), and not the other way around. Flipping it around can potentially confuse people and the endpoint itself could be misleading.
What do you think about something like: /v1/instance/{instance}/attach { disk: NameOrId } instead of /v1/disks/{disk}/attach { instance: NameOrId }
We can even go a bit further by having a very explicit endpoint like: /v1/instance/{instance}/attach-disk { disk: NameOrId }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you're saying. The challenge with attach and detach is that it's a two way action. It acts on both the disk and the instance. The benefit of the implementation I have is that there's only a single place to look for disks within the API. If clarification is the main sticking point it could just as well be updated to be /v1/disks/{disk}/attach-to-instance and /v1/disks/{disk}/detach-from-instance. If we go back to hanging it off of instances then I believe we should keep with the previous model of /instance/{instance}/disks/*.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to roll this back into the original style. I found an issue in my implementation that would require updates at the Nexus app layer to address which I'd rather not do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this is reverted to
/v1/instances/{instance}/disks/attach/v1/instances/{instance}/disks/detach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed with where you landed on this: I'm "talking to" the instance and the object of the attach/detach "verb" is the disk.
karencfv
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Another group of endpoints to tick off the list 🎉
Left a few comments/questions let me know what you think
| #[endpoint { | ||
| method = GET, | ||
| path = "/v1/disks/{disk}/metrics/{metric_name}", | ||
| tags = ["disks"], | ||
| }] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to take any action right now, but just as an FYI this endpoint is very likely to change.
| project_selector: Some(project_selector), | ||
| } => { | ||
| let disk = self | ||
| .project_lookup(opctx, project_selector)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering, how much work will be involved to change project_lookup to work without organizations?
Should the implementation change much? If so, how much effort would including organization in the lookup now and removing it almost immediately after take?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't be too much work. It's fairly formulaic.
| /// Attach a disk to an instance | ||
| #[endpoint { | ||
| method = POST, | ||
| path = "/v1/disks/{disk}/attach", | ||
| tags = ["disks"], | ||
| }] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately I don't think this approach will be as clear as the previous one. You generally attach a disk to an instance(vm), and not the other way around. Flipping it around can potentially confuse people and the endpoint itself could be misleading.
What do you think about something like: /v1/instance/{instance}/attach { disk: NameOrId } instead of /v1/disks/{disk}/attach { instance: NameOrId }
We can even go a bit further by having a very explicit endpoint like: /v1/instance/{instance}/attach-disk { disk: NameOrId }
| } | ||
| } | ||
|
|
||
| pub async fn project_delete_disk( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit and kind of a general comment: these used to be called project_$action_$subresource because they were nested under the Project, but that's no longer really true. It seems like all of these could now just be called $resource_$action (i.e., disk_delete() here). That probably applies to a bunch of other functions too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a note in the parent issue to tackle this as a follow up.
nexus/src/app/instance.rs
Outdated
| .fetch() | ||
| .await?; | ||
|
|
||
| let (.., authz_instance) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the disk is in a different Project? (Also, do we have a test for that?)
Before, it wasn't possible to specify this. Now it seems like it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the code here. It now uses a lookup for the instance and gets the authz_project then does a lookup for the disk with the project id as the root of the lookup. I think what you're saying still may apply though. If you try to attach a disk by UUID then the project id in the lookup would be ignored and the disk UUID would be used instead. I'll add a check for this, good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix is added in 8f79dc6. I only added it to the attach case because the detach case only operates on whats already attached so it's an unnecessary check there. In the error message I noticed that they must be in the same project. This technically tells the user something about the relationship of the resources they may not have known, but I think that's fine because the step before should ensure they have modify permissions on both resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great. I'm not sure what the plan is for TODO-v1 but this seems like a test we want sooner rather than later because it feels easy to break without noticing and it seems really bad if we accidentally ship it that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the TODO-v1 comments will be addressed before switching to the v1 endpoints.
| rqctx: Arc<RequestContext<Arc<ServerContext>>>, | ||
| path_params: Path<params::InstancePath>, | ||
| query_params: Query<params::OptionalProjectSelector>, | ||
| disk_to_attach: TypedBody<params::DiskPath>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to acknowledge that using DiskPath here is weird. My plan is to eventually convert all the *Path structs into *Identifier for consistency. Whether they're used in the path or provided via the body doesn't really matter given it's just different places to specify a resource.
| @@ -1,7 +1,9 @@ | |||
| API endpoints with no coverage in authz tests: | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is a mess. I will address it.
ahl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good; I think there's a problem with the disk list and disk list metrics endpoints
| #[serde(flatten)] | ||
| pub project_selector: ProjectSelector, | ||
| #[serde(flatten)] | ||
| pub pagination: PaginatedByName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as below: I'm not sure how this works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 7f608d1
| let handler = async { | ||
| let opctx = OpContext::for_external_api(&rqctx).await?; | ||
| let project_lookup = | ||
| nexus.project_lookup(&opctx, &query.project_selector)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is invalid when query.pagination is.page matches the WhichPage::Next variant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in Fixed in 7f608d1. I also updated the integration tests to verify
| #[derive(Deserialize, JsonSchema)] | ||
| pub struct DiskSelector { | ||
| #[serde(flatten)] | ||
| pub project_selector: Option<ProjectSelector>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we prefer this style to
| pub project_selector: Option<ProjectSelector>, | |
| pub project: Option<NameOrId>, | |
| pub organization: Option<NameOrId>, |
Does it allow for additional code sharing for these nested, flattened selector structures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By encoding a selector at every level (which itself is a resource + possible parent selector) we can flatten both the creation of selectors and the lookup functions down to only concerning itself with two cases. If you look at the endpoint handlers they're always using using a selector and potentially a path param. My thought here is that this general shape will be easier to encode in a macro. Once organization goes away and project is flattened a bit of the structure can be reduced though.
Co-authored-by: Adam Leventhal <ahl@oxide.computer>
|
I'm going to go ahead and merge this for the sake of forward progress. @ahl if you notice anything else let me know and I'll follow up on it. |
See #2028 This PR adds V1 versions of the developer facing networking APIs. Firewall rules aren't in this PR. I'll do that in a follow up. ## New Endpoints All endpoints here are scoped via query parameters by their parent selectors. So for `vpc-subnets` if you provide the vpc by name, you'll also need to provide the project. If the projected is specified by name you'll need the org. So `/v1/vpc-subnet?vpc={vpc_id}`, `/v1/vpc-subnet?vpc={vpc_name}&project={project_id}`, etc ```shell # Scoped via ?project={project} GET /v1/vpcs # List VPCs POST /v1/vpcs # Create a VPC GET /v1/vpcs/{vpc} # Get a VPC DELETE /v1/vpcs/{vpc} # Delete a VPC # Scoped via ?vpc={vpc} GET /v1/vpc-subnets # List VPC subnets POST /v1/vpc-subnets # Create a VPC subnet GET /v1/vpc-subnets/{subnet} # Get a VPC subnet DELETE /v1/vpc-subnets/{subnet} # Delete a VPC subnet # Scoped via ?vpc={vpc} GET /v1/vpc-routers # List VPCs POST /v1/vpc-routers # Create a VPC GET /v1/vpc-routers/{router} # Get a VPC DELETE /v1/vpc-routers/{router} # Delete a VPC # Scoped via ?router={router} GET /v1/vpc-router-routes # List VPC router routes POST /v1/vpc-router-routes # Create a VPC router route GET /v1/vpc-router-routes/{route} # Get a VPC router route DELETE /v1/vpc-router-routes/{route} # Delete a VPC router route # Scoped via ?instance={instance} GET /v1/network-interfaces # List network interfaces POST /v1/network-interfaces # Create network interface GET /v1/network-interfaces/{interface} # Get a network interface DELETE /v1/network-interfaces/{interface} # Delete network interface ``` For a quick overview of the entire API (and the changes introduced in this PR) check out the [nexus_tags.txt file](https://github.com/oxidecomputer/omicron/pull/2057/files#diff-482331ae638fae81e451868307fcd2f8836600a2ac552c965da987a402d94e54) ## Notable changes - I pulled app level network interface functions out into their [own file](https://github.com/oxidecomputer/omicron/pull/2057/files#diff-4d675c5f8ef51a7a12255d0a6cd593b4bfb7b77dbf5e791c97abf9e874a588bf). - I've slowly started to update the naming convention of app and datastore level functions to be less nested (and closer to the operation names) as per [this comment](#2008 (comment)). It's a little bit inconsistent now but the intent is to do a larger follow up to clean these all up. At present I'm just updating them as I come across them. - I've begun moving all possible contents of an http entrypoint into its handler for more timing information as per [this comment](#2206 (comment))
This PR is part of #2028
Changes
Old way of fetching disks
New ways of fetching disks
Fetching disks attached to an instance
Attaching and detaching disks to an instance
Disk metrics are not included in this PR anymore. There's some work necessary to smooth out pagination which may not be necessary with upcoming metrics changes.